package bsh;

import java.util.Iterator;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.compilation.ReflectionUtilsImpl;
import bsh.exception.CompilationFailedException;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.Scope;
import bsh.meta.ScopeType;
import bsh.meta.TypeWrapper;

/**
 * {@link NodeCompiler} for a {@link BSHEnhancedForStatement}
 * 
 * @author takeshi
 */
public class EnhancedForCompiler implements NodeCompiler {

    private final CompilationUnit unit;

    private final BSHEnhancedForStatement node;

    private static final MethodDescriptor GET_ITERABLE, ITERATOR_HAS_NEXT, ITERATOR_NEXT;

    static {
        ReflectionUtils utils = new ReflectionUtilsImpl();
        GET_ITERABLE =
                utils.findMethodForTypes(JavaClassWrapper.wrapperFor(CompiledScriptHelper.class), "getIterator",
                        new TypeWrapper[]{ JavaClassWrapper.OBJECT_WRAPPER });
        ITERATOR_HAS_NEXT = utils.findMethodForTypes(JavaClassWrapper.wrapperFor(Iterator.class), "hasNext", new TypeWrapper[]{});
        ITERATOR_NEXT = utils.findMethodForTypes(JavaClassWrapper.wrapperFor(Iterator.class), "next", new TypeWrapper[]{});
    }

    /**
     * C'tor
     * 
     * @param compilationUnit {@link CompilationUnit}
     * @param node {@link BSHEnhancedForStatement}
     */
    public EnhancedForCompiler(CompilationUnit compilationUnit, BSHEnhancedForStatement node) {
        super();
        this.unit = compilationUnit;
        this.node = node;
    }

    /**
     * {@inheritDoc}
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        Scope newScope = state.newScope(ScopeType.LOOP_BLOCK);
        Label beforeUpdate = newScope.getStartLabel();
        String elementName = this.node.varName;

        SimpleNode iterable, forBody = null;

        SimpleNode firstNode = ((SimpleNode) this.node.jjtGetChild(0));
        int nodeCount = this.node.jjtGetNumChildren();

        TypeWrapper elementType = JavaClassWrapper.OBJECT_WRAPPER;

        if (firstNode instanceof BSHType) {
            elementType = TypedVariableDeclarationCompiler.resolveTypeForNode((BSHType) firstNode, state.currentScope());
            iterable = ((SimpleNode) this.node.jjtGetChild(1));
            if (nodeCount > 2) {
                forBody = ((SimpleNode) this.node.jjtGetChild(2));
            }
        } else {
            iterable = firstNode;
            if (nodeCount > 1) {
                forBody = ((SimpleNode) this.node.jjtGetChild(1));
            }
        }

        // we'll compile the iterable
        int stack = state.getStackSize();
        this.unit.compileNode(iterable, host, state);
        state.assertStackSize(stack + 1); // and should be an object
        this.unit.getCompilerManager().coerceStackTopTo(host, JavaClassWrapper.OBJECT_WRAPPER, state);
        state.assertStackSize(stack + 1); // and should be an object
        // now we call Prototype.getIterator
        GET_ITERABLE.writeInvoke(host, state);
        state.assertStackSize(stack + 1); // and should be an Iterator
        this.unit.getCompilerManager().coerceStackTopTo(host, JavaClassWrapper.wrapperFor(Iterator.class), state);
        LocalVariable iterator = newScope.addLocalVariable(null, newScope.getStartLabel(), JavaClassWrapper.wrapperFor(Iterator.class));
        // store on an unnamed variable
        host.visitVarInsn(Opcodes.ASTORE, iterator.getOffset());
        state.popStack();
        state.assertStackSize(stack); //

        LocalVariable element = newScope.addLocalVariable(elementName, newScope.getStartLabel(), elementType);
        // the update must go here
        // host.visitJumpInsn(Opcodes.GOTO, beforeCondition);
        newScope.installStartLabel(host, state.getUnit().options());

        // the condition must go here
        host.visitVarInsn(Opcodes.ALOAD, iterator.getOffset());
        state.pushStack(iterator.getType());
        ITERATOR_HAS_NEXT.writeInvoke(host, state);
        this.unit.getCompilerManager().coerceToType(host, state.peekStack(), JavaClassWrapper.BOOLEAN_WRAPPER, state);

        state.assertStackTopType(JavaClassWrapper.BOOLEAN_WRAPPER);
        state.popStack();
        host.visitJumpInsn(Opcodes.IFEQ, newScope.getEndLabel());

        host.visitVarInsn(Opcodes.ALOAD, iterator.getOffset());
        state.pushStack(iterator.getType());
        ITERATOR_NEXT.writeInvoke(host, state);
        this.unit.getCompilerManager().coerceStackTopTo(host, elementType, state);
        host.visitVarInsn(Opcodes.ASTORE, element.getOffset());
        state.popStack();
        state.assertStackSize(stack);

        this.compileNodeClearStack(host, state, forBody);
        state.assertStackSize(stack);
        host.visitJumpInsn(Opcodes.GOTO, beforeUpdate);
        newScope.installEndLabel(host, this.unit.options());
        state.assertStackSize(stack);
        state.popScope();
    }

    /**
     * Compila o node, fazendo pop no stack se necessario
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param body {@link SimpleNode}
     * @throws CompilationFailedException e
     */
    private void compileNodeClearStack(MethodVisitor host, CompilationState state, SimpleNode body) throws CompilationFailedException {
        if (body != null) {
            this.unit.compileNode(body, host, state);
            CompilerImpl.popStackIfNeeded(host, state);
        }
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper nodeResultType(Scope scope) {
        return JavaClassWrapper.VOID_WRAPPER;
    }

}
